ScalaでActorを使ったメッセージパッシング便利化ライブラリを書いてみた


(旧)ScalaBaseに行けないエントリ (駄目な事書きすぎで事務所的にアウトが出ました)


概要

メッセージパッシング便利化ライブラリ作った

ScalaMessengerPrototype

https://github.com/sassembla/ScalaMessengerPrototype



Scalaで標準に用意されているActor(2.9.2までなのでActors)

http://www.scala-lang.org/api/current/scala/actors/Actor.html


を、特にclass間のmessagePassingに使用するために使いやすくしたつもりのライブラリ。

ちょうど2.10がそろそろ出るぜ的な話があったので、

Akkaに取って替わられるActorsにお別れを言いがてら作ってみた。



コンセプト/要件定義


MessagePasingが、特定のフォーマット+小さな手続きで使用/送受信可能


要件

・Javaから使える/Scalaから使える

・通信内部は全部Actors、Akka版へのリプレースを考慮に入れる

・同期、非同期でのメッセージ送付/受信

・ネスト呼び出し可

・ブロードキャストあり

・通信が可能な対象を明示する機構

・メッセージラベルの付与

・可変長引数でのオブジェクトの送付

・テスト書く

・動作にかかるコスト、処理のチューニングは無視



オリジナルは、Objective-CのNSNotificationを使ったもの。


このへん。

https://github.com/sassembla/MessengerSystem

注:古いです。ARC対応してません。ぶっちゃけARC対応した最新の社内版があるのだけど、特化しすぎてて出せない。



コレを使うと何がどう楽になるの?

メッセージパッシングのそもそもの利点として、

pointer/参照を持たないオブジェクト同士が、物を投げ合ったりする事が出来る


っつーのがあるのですが、

ただ、Actorsそのままだと、毎回定義するものが多かったり、case class作りまくらないといけなかったりで、めんどい。


なので、

数行書けば使える

みたいな目標をたてて、一通りMessagePassingが、

特定のフォーマット+小さな手続きで使用/送受信可能になるように、作ってました。


実際、Javaからだと6行くらい、Scalaからだと4行くらい書けば使えます。



列挙すると

①特定フォーマットでメッセージを送れる


def call(targetName : String, exec : String, message : Array[TagValue])


targetName

メッセージ送付ターゲットの識別子、メールでいうところのTo


exec

メッセージの識別子、メールでいうところのタイトル


message

可変長引数のkey-valueペア。本文みたいなもの。



②特定の受信箇所に届く(必ず書かなきゃいけない)

/**

* Childのレシーバ

*/

@Override

public void receiver(String exec, TagValue[] tagValues) {

.......


無いとコンパイルとおらない。



サンプルコード


Javaから使う

https://github.com/sassembla/ScalaMessengerPrototype/blob/master/sample/sampleJava/com/kissaki/Parent.java


https://github.com/sassembla/ScalaMessengerPrototype/blob/master/sample/sampleJava/com/kissaki/Child.java



Scalaから使う

https://github.com/sassembla/ScalaMessengerPrototype/blob/master/sample/sampleScala/com/kissaki/Parent.scala


https://github.com/sassembla/ScalaMessengerPrototype/blob/master/sample/sampleScala/com/kissaki/Child.scala




制約

PROBLEM:

pointerに依存せずに自由に物を送り合う事が出来るので、自由にやり過ぎるとカオスになる

受信者の名前だけを指定して送る形式だった時代があって、誰が誰に何を送ってるかがカオスになった。


SOLUTION;

明示した関係の間柄でしか、送付しあえないようにする



具体的には→


[親子関係がある間柄しかmessageを送付しあえない]という制約を勝手に作っている。



下記シークエンスで成立させてる。


・messengerが各自なまえを持つ

・子どもになる予定のmessengerからinputParent(親名称)で仮親を名前で指定

・親予定のmessengerが受信、条件を満たしていたら子予定を子どもとして認定、子どもに返信

・親子関係成立



→制約が有る上でのデメリット

・誰にでも送る、という動作ができなくなった

・本来余剰な機構なので重い



→制約がある上でのメリット

・誰に送るつもりか、コードの特定の箇所をみればわかる


デメリットの方が多いんだけど、複数人で使う上では、メッセージの方向の明示性は必須仕様だった。



MessagePassingで書くと楽なこと

・オブジェクト間の関係性をPointerとは無関係に再構築可能

Pointerでの構造的な構築とは別に、任意の意味的な関係の構築が可能。

既存の手段 : PointerでのObject所持、メソッド実行

に、

新手段 : MessagePassingでの関係構築

が加わる感じ。

道具が増える分だけ、明示できるものが増える。


・粗結合

MessagePassing自体の前提なので、コレを使って使われているライブラリとかモジュールも、粗結合にできる。

さらに言うとしやすい


・意味の付加が可能

発生させるメッセージを「イベント」だとすると、下記みたいな表記の発信と受け取りが可能。


リロードボタンが押されて、表示データが更新される


(ビュー:タッチが発生

→この部分はだいたいの仕様で暗黙のはず)


→ビューコントローラ:タッチイベントが発生

→RELOAD_TOUCHED をだれかに発信


→データ保存してるコントローラ:RELOAD_TOUCHEDを受け取る

→保存されてるデータを再取得(更新)

→更新されたデータを発信 DATA_RELOADED


→ビュー:DATA_RELOADED を受け取ってデータ表示更新


とりあえず書いてモヤッとしてるところ


Scalaからだと、使うオブジェクトがextends必須

→Javaから書いたので残ってしまった超残念な部分。Akkanizeさせたときに消す。


コアがシングルトンなobject

→一番なんとかならんかなーって思った箇所で、Actorsのchannelとかをちゃんと使えば出来たような気がします。

次必ず消します。


ScalaScalaしてない

→ログの機構とかにListBuffer使いまくってます。吊ってきます。


キャストって言う動作が完全に終わってる

→Akka版作るときは型変換効かせたいです。


Javaから使うのを優先しすぎた感ある

→Javaでテストを書いていたので、書きにくい形式を選べませんでした吊ってきます。

次回からScalaオンリーの予定。


遅い

→Javaから使って、messageが同期で届くまでに0.01秒くらいかかってる感じ。

ボトルネックは、通信のたびに小さな内部Actorを作っている事。


Actorsの実装だと、Actorから自分自身を呼び出すとロックする。

それを回避するために、自分自身へのメッセージ送付を肩代わりするActorが一瞬だけ生成される。


例えば下記の、親を呼ぶメソッドからは、



def callParent(exec : String, message : Array[TagValue]) = {

log += Log.LOG_TYPE_CALLPARENT.toString

val future = parent(0).duplicate !! CallParent(exec, message)

val result = future()


result match {

case Done(_) =>

case Failure(reason) =>

}

}


duplicateメソッドでActorを一つ作って、ネストした際のロックを避けるようになっている。



def duplicate : SubMessengerActor = {

new SubMessengerActor(this, myself)

}



で、SubMessengerActorの実装は、まんまActor。


class SubMessengerActor(master : MessengerActor, myself : MessengerProtocol) extends Actor {

start


def act() = {

loop {

...



Actorの中に、通信のたびに別のActorが生まれて、終わったら消える。

こんなのがメッセージごとにあるので、めっちゃコスト喰うし、遅い。


Akkaだとネストしても平気なので、デフォで似たような機構が入ってるのかもしれないけど、最適化とかいろいろホラ。。


GCあるのに、Dealloc相当のメソッドがある

→Actorの都合上、停止は明示的に書かないといけないです。

このライブラリのMessengerも、

closeでMessengerだけを、

closeSystemで「系」全体を停止させます。


どうすればいいのかわかりません。GCさんよろしくお願いします、、よろ、、し、、、



今後

Akka化にあたって、サーバ間でのremoteをサポートして、remote間でのMessagingを簡単にする。


社内サービスで使うつもり。





ざっくり、以上。


それこんなライブラリ使うともっと簡単にできるよ、とか、

車輪の再発明乙、とかだったら切ないので教えてもらえるとたすかりんこ。